# -*- coding: utf-8 -*-

"""
@author: bw
@contact: bw@handsdriver.net
"""

import itertools as I

import sys, os, errno

import contextlib
import urllib.request as urllib

import plumbum.commands  as _plmcmd
import plumbum.path.base as _plmpath
import plumbum.lib       as _plmlib


class StatRes:
	"""POSIX-like stat result"""

	def __init__(self, tup):
		self._tup = tuple(tup)

	def __getitem__(self, index):
		return self._tup[index]

	st_mode = mode = property(lambda self: self[0])
	st_ino = ino = property(lambda self: self[1])
	st_dev = dev = property(lambda self: self[2])
	st_nlink = nlink = property(lambda self: self[3])
	st_uid = uid = property(lambda self: self[4])
	st_gid = gid = property(lambda self: self[5])
	st_size = size = property(lambda self: self[6])
	st_atime = atime = property(lambda self: self[7])
	st_mtime = mtime = property(lambda self: self[8])
	st_ctime = ctime = property(lambda self: self[9])


class VagrantWorkdir:
	"""Working directory manipulator."""

	counter = I.count().__next__

	def __init__(self, machine):
		self.__stack = [(self.counter(), VagrantPath(machine))]

	def __hash__(self):
		raise TypeError('Unhashable type')

	def __eq__(self, other):
		if not isinstance(other, (str, VagrantPath)):
			raise TypeError(type(other))
		return self.getpath() == other

	def __str__(self):
		return str(self.getpath())

	def __repr__(self):
		return '<VagrantWorkdir "{!s}">'.format(self.getpath())

	@contextlib.contextmanager
	def __call__(self, newdir):
		newpath = self.getpath().join(newdir)
		try:
			self.__stack.append((self.counter(), newpath))
			yield newpath  # @todo: (?) VagrantWorkdir
		finally:
			self.__stack.pop(-1)

	def change(self, value, level=None):
		if isinstance(value, (str, VagrantPath)):
			if level is None:
				lvl, path = self.__stack[-1]
				self.__stack[-1] = lvl, path.join(value)
			else:
				for idx, (lvl, path) in enumerate(self.__stack):
					if lvl == level:
						if path != value:
							self.__stack[idx] = level, path.join(value)
						break

	def getpath(self):
		return self.__stack[-1][1]

	def getlevel(self):
		return self.__stack[-1][0]


class VagrantPath(_plmpath.Path):
	"""The class implementing remote-machine paths"""

	CASE_SENSITIVE = True

	machine  = None
	encoding = None
	sep      = None
	pathsep  = None

	def __new__(cls, machine, *parts):
#		if not parts:
#			raise TypeError('At least one path part is required (none given)')

		normed  = []

		parts = tuple(map(str, parts))  # force the paths into string, so subscription works properly
		# Simple skip if path is absolute

		if not parts or parts[0] and parts[0][0] != machine.sep:
#			cwd   = machine._cwd if hasattr(machine, '_cwd') else machine.getSession().run('pwd')[1].strip()
			cwd   = machine.getSession().run('pwd')[1].strip()
			parts = (cwd,) + parts

		for part in parts:
			if part[:1] == machine.sep:
				del normed[:]

			plist = str(part).split(machine.sep)

			if not plist[0]:
				plist.pop(0)
				del normed[:]

			for item in plist:
				if item == '' or item == '.':
					continue
				if item == '..':
					if normed:
						normed.pop(-1)
				else:
					normed.append(item)

		self = super().__new__(cls, machine.sep + machine.sep.join(normed))

		self.machine  = machine
		self.encoding = machine.custom_encoding
		self.sep      = machine.sep
		self.pathsep  = machine.pathsep

		assert self.machine.uname.lower() != 'windows'
		assert self.sep     == '/'  # @see: `shlex`
		assert self.pathsep == ':'

		return self

	def _get_info(self):
		return (self.machine, str(self))

	def _form(self, *parts):
		return VagrantPath(self.machine, *parts)

#	@property
#	def _path(self):
#		return str(self)

	@property  # type: ignore
	@_plmlib._setdoc(_plmpath.Path)
	def name(self):
		if self.sep not in str(self):
			return str(self)
		return str(self).rsplit(self.sep, 1)[1]

	@property  # type: ignore
	@_plmlib._setdoc(_plmpath.Path)
	def dirname(self):
		if self.sep not in str(self):
			return str(self)
		return VagrantPath(self.machine, str(self).rsplit(self.sep, 1)[0])

	@property  # type: ignore
	@_plmlib._setdoc(_plmpath.Path)
	def suffix(self):
		return '.' + self.name.rsplit('.', 1)[1]

	@property  # type: ignore
	@_plmlib._setdoc(_plmpath.Path)
	def suffixes(self):
		name = self.name
		exts = []
		while '.' in name:
			name, ext = name.rsplit('.', 1)
			exts.append('.' + ext)
		return list(reversed(exts))

#	@property  # type: ignore
#	@_plmlib._setdoc(_plmpath.Path)
#	def uid(self):
#		uid, name = self.machine._path_getuid(self)
#		return _plmpath.FSUser(int(uid), name)
#	@property  # type: ignore
#	@_plmlib._setdoc(_plmpath.Path)
#	def gid(self):
#		gid, name = self.machine._path_getgid(self)
#		return _plmpath.FSUser(int(gid), name)

	@_plmlib._setdoc(_plmpath.Path)
	def join(self, *parts):
		return VagrantPath(self.machine, self, *parts)

	@_plmlib._setdoc(_plmpath.Path)
	def with_name(self, name):
		return VagrantPath(self.machine, self.dirname) / name

	@_plmlib._setdoc(_plmpath.Path)
	def with_suffix(self, suffix, depth=1):
		if suffix and not suffix.startswith('.') or suffix == '.':
			raise ValueError('Invalid suffix', suffix)

		name  = self.name
		depth = len(self.suffixes) if depth is None else min(depth, len(self.suffixes))

		for i in range(depth):
			name, ext = name.rsplit('.', 1)

		return VagrantPath(self.remote, self.dirname) / (name + suffix)

#	@_plmlib._setdoc(_plmpath.Path)
#	def list(self):
#		if not self.is_dir():
#			return []
#		return [self.join(fn) for fn in self.machine._path_listdir(self)]
#	@_plmlib._setdoc(_plmpath.Path)
#	def iterdir(self):
#		if not self.is_dir():
#			return ()
#		return (self.join(fn) for fn in self.machine._path_listdir(self))
#	@_plmlib._setdoc(_plmpath.Path)
#	def is_dir(self):
#		res = self.machine._path_stat(self)
#		if not res:
#			return False
#		return res.text_mode == 'directory'
#	@_plmlib._setdoc(_plmpath.Path)
#	def is_file(self):
#		res = self.machine._path_stat(self)
#		if not res:
#			return False
#		return res.text_mode in ('regular file', 'regular empty file')
#	@_plmlib._setdoc(_plmpath.Path)
#	def is_symlink(self):
#		res = self.machine._path_stat(self)
#		if not res:
#			return False
#		return res.text_mode == 'symbolic link'

	@_plmlib._setdoc(_plmpath.Path)
	def exists(self):
		try:
			self.stat()
		except OSError:
			return False
		return True

	@_plmlib._setdoc(_plmpath.Path)
	def stat(self):
		if self.machine.uname not in ('Darwin', 'FreeBSD'):
			stat_cmd = 'stat -c "%F,%f,%i,%d,%h,%u,%g,%s,%X,%Y,%Z" '
		else:
			stat_cmd = 'stat -f "%HT,%Xp,%i,%d,%l,%u,%g,%z,%a,%m,%c" '

		rc, out, _ = self.machine.getSession().run(stat_cmd + _plmcmd.shquote(self), retcode=None)

		if rc != 0:
			raise OSError(errno.ENOENT, os.strerror(errno.ENOENT), '')

		statres = out.strip().split(',')
		text_mode = statres.pop(0).lower()

		res = StatRes((int(statres[0], 16),) + tuple(int(sr) for sr in statres[1:]))
		res.text_mode = text_mode

		return res

	@_plmlib._setdoc(_plmpath.Path)
	def access(self, mode=0):
		try:
			statres = self.stat()
		except OSError:
			return False

		mode = self._access_mode_to_flags(mode)
		mask = statres.st_mode & 0x1ff

		return ((mask >> 6) & mode) or ((mask >> 3) & mode)

#	@_plmlib._setdoc(_plmpath.Path)
#	def glob(self, pattern):
#		fn = lambda pat: [VagrantPath(self.remote, m) for m in self.machine._path_glob(self, pat)]
#		return self._glob(pattern, fn)

	@_plmlib._setdoc(_plmpath.Path)
	def delete(self):
		self.machine.getSession().run('rm -rf ' + _plmcmd.shquote(self))

	unlink = delete

	"""
    @_plmlib._setdoc(_plmpath.Path)
    def move(self, dst):
        if isinstance(dst, VagrantPath):
            if dst.remote is not self.remote:
                raise TypeError("dst points to a different remote machine")
        elif not isinstance(dst, str):
            raise TypeError(
                "dst must be a string or a VagrantPath (to the same remote machine), "
                "got %r" % (dst, ))
        self.remote._path_move(self, dst)

    @_plmlib._setdoc(_plmpath.Path)
    def copy(self, dst, override=False):
        if isinstance(dst, VagrantPath):
            if dst.remote is not self.remote:
                raise TypeError("dst points to a different remote machine")
        elif not isinstance(dst, str):
            raise TypeError(
                "dst must be a string or a VagrantPath (to the same remote machine), "
                "got %r" % (dst, ))
        if override:
            if isinstance(dst, str):
                dst = VagrantPath(self.remote, dst)
            dst.remove()
        else:
            if isinstance(dst, str):
                dst = VagrantPath(self.remote, dst)
            if dst.exists():
                raise TypeError("Override not specified and dst exists")

        self.remote._path_copy(self, dst)

    @_plmlib._setdoc(_plmpath.Path)
    def mkdir(self, mode=None, parents=True, exist_ok=True):
        if parents and exist_ok:
            self.remote._path_mkdir(self, mode=mode, minus_p=True)
        else:
            if parents and len(self.parts) > 1:
                self.remote._path_mkdir(self.parent, mode=mode, minus_p=True)
            try:
                self.remote._path_mkdir(self, mode=mode, minus_p=False)

            except _plmcmd.ProcessExecutionError:
                _, ex, _ = sys.exc_info()
                if "File exists" in ex.stderr:
                    if not exist_ok:
                        raise OSError(
                            errno.EEXIST, "File exists (on remote end)",
                            str(self))
                else:
                    raise
	"""

	@_plmlib._setdoc(_plmpath.Path)
	def read(self, encoding=None):
		# @todo: pipe
		data = self.machine.getSession().run('cat ' + _plmcmd.shquote(self))

		if self.encoding and isinstance(data, str):
			data = data.encode(self.machine.custom_encoding)

		return data

#	@_plmlib._setdoc(_plmpath.Path)
#	def write(self, data, encoding=None):
#		if encoding:
#			data = data.encode(encoding)
#		self.machine._path_write(self, data)

	@_plmlib._setdoc(_plmpath.Path)
	def touch(self):
		self.machine.getSession().run('touch ' + _plmcmd.shquote(self))

	"""
    @_plmlib._setdoc(_plmpath.Path)
    def chown(self, owner=None, group=None, recursive=None):
        self.remote._path_chown(
            self, owner, group,
            self.is_dir() if recursive is None else recursive)

    @_plmlib._setdoc(_plmpath.Path)
    def chmod(self, mode):
        self.remote._path_chmod(mode, self)

    @_plmlib._setdoc(_plmpath.Path)
    def link(self, dst):
        if isinstance(dst, VagrantPath):
            if dst.remote is not self.remote:
                raise TypeError("dst points to a different remote machine")
        elif not isinstance(dst, str):
            raise TypeError(
                "dst must be a string or a VagrantPath (to the same remote machine), "
                "got %r" % (dst, ))
        self.remote._path_link(self, dst, False)

    @_plmlib._setdoc(_plmpath.Path)
    def symlink(self, dst):
        if isinstance(dst, VagrantPath):
            if dst.remote is not self.remote:
                raise TypeError("dst points to a different remote machine")
        elif not isinstance(dst, str):
            raise TypeError("dst must be a string or a VagrantPath (to the same remote machine), got %r" % (dst,))
        self.remote._path_link(self, dst, True)
	"""

	def open(self, mode='r', bufsize=-1):
#		if hasattr(self.remote, "sftp") and hasattr(self.remote.sftp, "open"):
#			return self.remote.sftp.open(self, mode, bufsize)
		raise NotImplementedError

	@_plmlib._setdoc(_plmpath.Path)
	def as_uri(self, scheme='ssh'):
#		return '{0}://{1}{2}'.format(scheme, self.remote._fqhost, urllib.pathname2url(str(self)))
		raise NotImplementedError

	@property  # type: ignore
	@_plmlib._setdoc(_plmpath.Path)
	def stem(self):
		return self.name.rsplit('.')[0]

	@property  # type: ignore
	@_plmlib._setdoc(_plmpath.Path)
	def root(self):
		# @xxx: Windows
		return self.sep

	@property  # type: ignore
	@_plmlib._setdoc(_plmpath.Path)
	def drive(self):
		# @xxx: Windows
		return ''
